Skip to content

fix(spec): harden for leanSpec 4ca7d27 (equivocation determinism, future-horizon, off-finalized admission, justification rejections)#367

Merged
mananuf merged 7 commits into
devnet-5from
fix/spec-hardening-4ca7d27
Jul 7, 2026
Merged

fix(spec): harden for leanSpec 4ca7d27 (equivocation determinism, future-horizon, off-finalized admission, justification rejections)#367
mananuf merged 7 commits into
devnet-5from
fix/spec-hardening-4ca7d27

Conversation

@mananuf

@mananuf mananuf commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Spec-compliance hardening for leanSpec 4ca7d27

Bumps LEAN_SPEC_COMMIT_HASH to 4ca7d27 (leanSpec HEAD) and closes the gean drift a /spec-compliant sweep across the 7 commits 43246bd..4ca7d27 surfaced. All 7 are hardening/correctness fixes (DoS bounds, determinism, typed rejections); the happy path is unchanged.

Spec-compliance fixes

Commit leanSpec Change
ad262a5 #1181 Equal-slot equivocation determinism. ExtractLatestAttestations now keeps, per validator, the vote with the highest (slot, attestation-data hash-tree-root), so the extracted LMD view is a pure function of the pool, independent of insertion order. Block production already applied the same canonical-root secondary sort key.
e27dc7c #1182 BLOCK_TOO_FAR_IN_FUTURE. Reject a block whose slot exceeds the store clock by more than one slot, in on_block before the transition and signature check, so a far-future block cannot force an unbounded empty-slot walk. currentSlot = store.Time()/IntervalsPerSlot; the full-slot margin still admits an intended early block.
b625e80 #1179 HEAD_NOT_DESCENDANT_OF_FINALIZED. Attestation admission rejects a vote whose head does not descend from the finalized block, mirroring the prune predicate so a re-gossiped below-finalized aggregate cannot re-enter the pool after pruning.
24d6621 #1178 Typed justification-state rejections. Reject a zero-validator registry (EMPTY_VALIDATOR_REGISTRY) instead of silently no-oping, and reject a vote bitlist whose length ≠ len(justifications_roots) × validator_count (JUSTIFICATION_VOTES_LENGTH_MISMATCH), rather than reading past/short of a segment. The existing zero-root check is retained, in spec order.

Each change mirrors its leanSpec function precisely and was self-reviewed for spec mapping, invariants, and gean's architecture (pure statetransition, off-tick workers, ForkChoice outside the store).

Spectest harness catch-up

The pin bump exposed two spectest issues unrelated to the fixes above — both spectest-harness catch-ups for the #1178/#1179 fixture reworks, not consensus-logic bugs:

Commit Change
905c490 Signed justifiability delta. Fixtures now carry delta = slot - finalizedSlot as a signed value (negative before the finalized boundary). Typed the field int64 + signed cross-check. gean's SlotIsJustifiableAfter was already correct.
b3ca0f5 Known-vote re-sync. The reworked store-pruning fixture expects a vote gossiped as "new" to be "known" after the next block step. The harness promoted the payload pool but left the fork-choice tracker unchanged; now re-derives known votes from the promoted pool, mirroring the Engine's next updateHead.

Validation

  • make build ✓ · make test ✓ · full spec suite ok internal/spectests ✓ · make lint
  • Fixture-proven: #1182 (2 future-horizon vectors) and #1179 (the re-gossip-pruned-vote = HEAD_NOT_DESCENDANT_OF_FINALIZED) flip fail→pass; #1178 passing; zero regressions across the suite.
  • #1181 (equivocation) is validated by gean unit tests (store/payloads, the equal-slot max-(slot,root) selection) and matches the fixture's documented intent.

Known upstream caveat

leanSpec's own #1181 equivocation fixtures fail to generate at 4ca7d27 — its fork-choice produces a head that disagrees with its own test's expected value (head_slot actual ≠ expected). This is an upstream leanSpec inconsistency (its #1181 code vs its #1181 test), not a gean issue: fixture generation runs leanSpec's pytest, which gean only consumes. The 3 equivocation vectors are therefore excluded from the run; everything else generates and passes. Worth filing upstream.

Notes

  • Pin bump is its own commit; each spec fix is a separate, revertible commit.
  • No generated SSZ was hand-edited; no other-client references in comments.

mananuf added 7 commits July 7, 2026 00:54
Mirror leanSpec #1181: an equivocator can cast two distinct votes at one
slot. ExtractLatestAttestations now keeps, per validator, the vote with the
highest (slot, attestation-data hash-tree-root), so the extracted LMD view is
a pure function of the pool, independent of insertion order, and every node
runs fork choice on the same set. Block production already applied the same
canonical-root secondary sort key.
Mirror leanSpec #1182: reject a block whose slot exceeds the store clock by
more than one slot (BLOCK_TOO_FAR_IN_FUTURE), in on_block before the transition
and signature check, so a far-future block cannot force an unbounded empty-slot
walk. currentSlot = store.Time()/IntervalsPerSlot; the full-slot margin still
admits an intended early block. The signatures spectest now sets the store clock
to the block's slot, as the live node does before importing an on-time block.
Mirror leanSpec #1179: attestation admission now rejects a vote whose head does
not descend from the finalized block (HEAD_NOT_DESCENDANT_OF_FINALIZED),
mirroring the prune predicate so a re-gossiped below-finalized aggregate cannot
re-enter the pool after pruning dropped it.
… errors

Mirror leanSpec #1178: reject a zero-validator registry (EMPTY_VALIDATOR_REGISTRY)
instead of silently no-oping, and reject a justification vote bitlist whose length
is not len(justifications_roots) * validator_count
(JUSTIFICATION_VOTES_LENGTH_MISMATCH) rather than reading past or short of a
segment. The existing zero-root check is retained, in spec order.
At pin 4ca7d27 the justifiability fixtures carry delta = slot - finalizedSlot as
a signed value (negative for slots before the finalized boundary). Type the
fixture field int64 and compute the cross-check delta with signed subtraction;
gean's SlotIsJustifiableAfter already returns false for slot < finalizedSlot, so
this is a fixture-parser catch-up only.
The #1179-reworked store-pruning fixture expects a vote gossiped as "new" to be
"known" after the next block step. The harness promoted the payload pool
(PromoteNewToKnown) but left the fork-choice known tracker unchanged, so the
just-promoted vote was absent from the tracker the attestationChecks read.
Re-derive the known votes from the promoted pool, mirroring the Engine's next
updateHead.
@mananuf mananuf merged commit 693c40f into devnet-5 Jul 7, 2026
@mananuf mananuf deleted the fix/spec-hardening-4ca7d27 branch July 7, 2026 12:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant